/*
 * Copyright (c) 2014, Hsu Shih-Chia (schsu01@gmail.com)
 * All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.qood;

import java.util.*;
import java.util.regex.Pattern;

/**
 * QWhere is similar to <a href="http://en.wikipedia.org/wiki/Where_(SQL)">WHERE</a>
 * or <a href="http://en.wikipedia.org/wiki/Having_(SQL)">HAVING</a> clause in SQL.
 * <p/>
 * <p>Each condition is a node in tree, whose parent branch is AND/ OR/ NOT provided by {@link Logic}.
 *
 * @author SCHsu01
 * @version 1.0
 */
public interface QWhere {
  /**
   * Stop setting current condition, and back to parent branch
   *
   * @return parent branch
   */
  Logic end();

  /**
   * Provide WHERE branch like "AND(~)", "OR(~)", "NOT(~)", for adding other WHERE nodes.
   */
  interface Logic extends QWhere {
    /**
     * Create a WHERE branch
     *
     * @return AND(~)
     */
    Logic matchAll();

    /**
     * Create a WHERE branch
     *
     * @return OR(~)
     */
    Logic matchAny();

    /**
     * Create a WHERE branch
     *
     * @return NOT(~)
     */
    Logic matchNone();

    /**
     * Create a condition to check if this formula (or column) in given values
     *
     * @param formula column or formula
     * @param <T>     force data type of given values
     * @return IN(~)
     */
    <T> In<T> in(String formula);

    /**
     * Create a condition to check if this formula (or column) between given values
     *
     * @param formula column or formula
     * @param <T>     force data type of given values
     * @return ? BETWEEN ~ AND ~
     */
    <T extends Comparable> Range<T> range(String formula);

    /**
     * Create a condition to check if the text of this formula (or column) starts/endsWith or contains given texts
     *
     * @param formula column or formula
     * @return ? LIKE ~
     */
    LikeText likeText(String formula);

    /**
     * Create a condition to check if the text of this formula (or column) matches given globbing/regex patterns
     *
     * @param formula column or formula
     * @return ? LIKE ~
     */
    LikePattern likePattern(String formula);

    /**
     * Add a customized filter condition
     *
     * @param rule customized filter, normally extends {@link Custom.Template}
     * @return a reference to this Object
     */
    Logic add(Custom rule);
  }

  /**
   * Provide WHERE node like <a href="http://en.wikipedia.org/wiki/Where_(SQL)#IN">"FIELD IN (~)" or "FIELD = ~"</a>.
   */
  interface In<T> extends QWhere {
    /**
     * Add a value for matching, eg: "FIELD = 1"
     *
     * @param value allow null
     * @return a reference to this Object
     */
    In<T> value(T value);

    /**
     * Add values for matching, eg: "FIELD IN (1,2,3)"
     *
     * @param values values
     * @return a reference to this Object
     */
    In<T> values(T... values);

    /**
     * Add values for matching, eg: "FIELD IN (1,2,3)"
     *
     * @param values Collection
     * @return a reference to this Object
     */
    In<T> values(Collection<T> values);

    /**
     * Add a column or formula for matching, eg: "FIELD = OTHER_FIELD"
     *
     * @param formula a column or formula
     * @return a reference to this Object
     */
    In<T> formula(String formula);

    /**
     * Add columns or formulas for matching, eg: "FIELD IN (FIELD1, FIELD2)"
     *
     * @param formula some columns or formulas
     * @return a reference to this Object
     */
    In<T> formula(String... formula);
  }

  /**
   * Provide WHERE node like <a href="http://en.wikipedia.org/wiki/Where_(SQL)#BETWEEN">
   * "FIELD BETWEEN ~ AND ~"</a>, "&lt;= ~", "&gt;= ~", "&lt; ~" or "&gt; ~".
   */
  interface Range<T extends Comparable> extends QWhere {
    /**
     * Set whether use "equals" while comparing
     *
     * @param allow (true by default) "min &lt;= FIELD &lt;= max" or (false) "min &lt; FIELD &lt; max"
     * @return a reference to this Object
     */
    Range<T> allowEquals(boolean allow);

    /**
     * Set min value for comparing, eg: "FIELD &gt;= min"
     *
     * @param value min value
     * @return a reference to this Object
     */
    Range<T> min(T value);

    /**
     * Set max value for comparing, eg: "FIELD &lt;= max"
     *
     * @param value max value
     * @return a reference to this Object
     */
    Range<T> max(T value);

    /**
     * Set min column or formula for comparing, eg: "FIELD &gt;= MIN_FIELD"
     *
     * @param formula min column or formula
     * @return a reference to this Object
     */
    Range<T> minBy(String formula);

    /**
     * Set max column or formula for comparing, eg: "FIELD &lt;= MAX_FIELD"
     *
     * @param formula min column or formula
     * @return a reference to this Object
     */
    Range<T> maxBy(String formula);
  }

  /**
   * Provide WHERE node like <a href="http://en.wikipedia.org/wiki/Where_(SQL)#LIKE">"FIELD LIKE ~"</a>,
   * but support "text%", "%text" and "%text%" only.
   */
  interface LikeText extends QWhere {
    /**
     * Set whether be case sensitive
     *
     * @param ignore (true) case ignored (false by default) case sensitive
     * @return a reference to this Object
     */
    LikeText ignoreCase(boolean ignore);

    /**
     * Add patterns for matching, eg: "FIELD LIKE 'text%' OR ~"
     *
     * @param text starts with text without '%' or '*'
     * @return a reference to this Object
     */
    LikeText startWith(String... text);

    /**
     * Add columns or formulas for matching, eg: "FIELD LIKE OTHER_FIELD || '%' OR ~"
     *
     * @param formula starts with column or formula
     * @return a reference to this Object
     */
    LikeText startBy(String... formula);

    /**
     * Add patterns for matching, eg: "FIELD LIKE '%text%' OR ~"
     *
     * @param text contains text without '%' or '*'
     * @return a reference to this Object
     */
    LikeText hasWith(String... text);

    /**
     * Add columns or formulas for matching, eg: "FIELD LIKE '%' || OTHER_FIELD || '%' OR ~"
     *
     * @param formula contains column or formula
     * @return a reference to this Object
     */
    LikeText hasBy(String... formula);

    /**
     * Add patterns for matching, eg: "FIELD LIKE '%text' OR ~"
     *
     * @param text ends with text without '%' or '*'
     * @return a reference to this Object
     */
    LikeText endWith(String... text);

    /**
     * Add columns or formulas for matching, eg: "FIELD LIKE '%' || OTHER_FIELD OR ~"
     *
     * @param formula ends with column or formula
     * @return a reference to this Object
     */
    LikeText endBy(String... formula);
  }

  /**
   * Provide WHERE node like <a href="http://en.wikipedia.org/wiki/Where_(SQL)#LIKE">"FIELD LIKE ~"</a>,
   * supports both <a href="http://en.wikipedia.org/wiki/Glob_(programming)">Glob Patterns</a>
   * (aka <a href="http://en.wikipedia.org/wiki/Wildcard_character">Wildcards</a>)
   * and <a href="http://en.wikipedia.org/wiki/Regular_expression">Regular Expression</a>.
   */
  interface LikePattern extends QWhere {
    /**
     * Add a pattern for matching, eg: "FIELD LIKE 'text*'"
     *
     * @param globPattern contains '*', '?'
     * @return a reference to this Object
     */
    LikePattern glob(String globPattern);

    /**
     * Add patterns for matching, eg: "FIELD LIKE 'text*' OR FIELD LIKE '*text'"
     *
     * @param globPattern contains '*', '?'
     * @return a reference to this Object
     */
    LikePattern glob(String... globPattern);

    /**
     * Add a regular expression pattern for matching
     *
     * @param regexpPattern regular expression
     * @return a reference to this Object
     */
    LikePattern regexp(String regexpPattern);

    /**
     * Add regular expression pattern for matching
     *
     * @param regexpPattern regular expressions
     * @return a reference to this Object
     */
    LikePattern regexp(String... regexpPattern);
  }

  /**
   * Provide WHERE node of customized filter rule by your implementation.
   */
  interface Custom {
    /**
     * Defines what should be gathered for filtering
     *
     * @return columns or formulas
     */
    String[] getFormula();

    /**
     * Please implement this method to determine whether to include this row.
     *
     * @param value contents in this row by {@link #getFormula()} you defined
     * @return Whether to include this row, filtering stops until returns null
     */
    Boolean is(Object[] value);

    /**
     * You can have a {@link Custom} filter rule implementation by extending this template class.
     * <p/>
     * <p><pre><code>
     * sql.where(new QWhere.Custom.Template("COLUMN1","INT(COLUMN2)"){//anonymous class
     *    //your implementation
     * });</code></pre>
     */
    abstract class Template implements Custom {
      private final String[] formula;

      protected Template(String... formula) {
        this.formula = formula;
      }

      public final String[] getFormula() {
        return formula;
      }
    }
  }
}

interface Filter {
  /**
   * @param row rowId for filtering
   * @return null=skip all rows below, TRUE=include, FALSE=exclude
   */
  Boolean is(int row);
}

abstract class Where implements QWhere {
  WhereLogic parent;

  Where(WhereLogic parent) {
    this.parent = parent;
    if (null != parent) parent.add(this);
  }

  abstract Filter createFilter(Temp view);//key feature

  public QWhere.Logic end() {
    return null == parent ? (parent = new WhereLogic(null, WhereLogic.MATCH_ALL).add(this)) : parent;
  }
}

final class WhereLogic extends Where implements QWhere.Logic {
  static final boolean[]
    MATCH_ALL = {false, false},//if any==F return F, finally return T
    MATCH_ANY = {true, true},  //if any==T return T, finally return F
    MATCH_NONE = {false, true};//if any==F return T, finally return F
  private final boolean expect, result;
  private final List<Where> filters = new ArrayList<Where>();

  WhereLogic(WhereLogic parent, boolean[] matchType) {
    super(parent);
    this.expect = matchType[0];
    this.result = matchType[1];
  }

  public WhereLogic matchAll() {
    return expect || result ? new WhereLogic(this, MATCH_ALL) : this;//flatten
  }

  public WhereLogic matchAny() {
    return expect && result ? this : new WhereLogic(this, MATCH_ANY);//flatten
  }

  public WhereLogic matchNone() {
    return new WhereLogic(this, MATCH_NONE);
  }

  WhereLogic add(QWhere where) {
    filters.add((Where) where);
    return this;
  }

  public <T> In<T> in(String formula) {
    return new WhereIn<T>(this, formula);
  }

  public <T extends Comparable> Range<T> range(String formula) {
    return new WhereRange<T>(this, formula);
  }

  public LikeText likeText(String formula) {
    return new WhereLike(this, formula);
  }

  public LikePattern likePattern(String formula) {
    return new WherePattern(this, formula);
  }

  public Logic add(Custom rule) {
    new WhereCustom(this, rule);
    return this;
  }

  @Override
  public QWhere.Logic end() {
    return null == parent ? this : parent;
  }

  @Override
  public String toString() {
    return StringUtil.join(filters.iterator(), expect ? " OR " : " AND ")
      .insert(0, expect == result ? "(" : "NOT(").append(')').toString();
  }

  Filter createFilter(Temp view) {
    final int n = filters.size();
    if (1 == n && expect == result) return filters.get(0).createFilter(view);//1 condition in and/or
    final Filter[] f = new Filter[n];
    for (int i = 0; i < n; i++) f[i] = filters.get(i).createFilter(view);
    return new FilterLogic(expect, result, f);
  }

  private static final class FilterLogic implements Filter {
    private final boolean expect, result;
    private final Filter[] f;

    private FilterLogic(boolean expect, boolean result, Filter[] f) {
      this.expect = expect;
      this.result = result;
      this.f = f;
    }

    public Boolean is(int row) {
      for (final Filter filter : f) {
        final Boolean ret = filter.is(row);
        if (null == ret) return null;
        if (expect == ret) return result;
      }
      return !result;
    }
  }
}

final class WhereIn<T> extends Where implements QWhere.In<T> {
  private final String formula;
  private final Collection<T> values = new HashSet<T>();
  private final Collection<String> formulas = new HashSet<String>();

  WhereIn(WhereLogic parent, String formula) {
    super(parent);
    this.formula = formula;
  }

  public WhereIn<T> value(T value) {
    values.add(value);
    return this;
  }

  public WhereIn<T> values(T... values) {
    return values(Arrays.asList(values));
  }

  public WhereIn<T> values(Collection<T> coll) {
    values.addAll(coll);
    return this;
  }

  public WhereIn<T> formula(String formula) {
    formulas.add(formula);
    return this;
  }

  public WhereIn<T> formula(String... formula) {
    formulas.addAll(Arrays.asList(formula));
    return this;
  }

  @Override
  public String toString() {
    final StringBuilder result = new StringBuilder(formula).append(" IN (")
      .append(StringUtil.join(values.iterator(), ", "));
    if (!formulas.isEmpty()) result.append(", ").append(StringUtil.join(formulas.iterator(), ", "));
    return result.append(')').toString();
  }

  Filter createFilter(Temp view) {
    return new FilterIn(view.getCol(formula), values, formulas.isEmpty() ?
      null : view.getCols(StringUtil.toArray(formulas)));
  }

  private static final class FilterIn implements Filter {
    private final Col get;
    private final Collection values;
    private final Col[] cols;

    private FilterIn(Col get, Collection values, Col[] cols) {
      this.get = get;
      this.values = values;
      this.cols = cols;
    }

    public Boolean is(int row) {
      final Object value = get.getRow(row);
      if (values.contains(value)) return true;
      final boolean isNull = null == value;
      if (null != cols) for (final Col col : cols)
        if (isNull ? null == col.getRow(row) : value.equals(col.getRow(row))) return true;
      return false;
    }
  }
}

final class WhereRange<T extends Comparable> extends Where implements QWhere.Range<T> {
  private final String formula;
  private T min, max;
  private String minF, maxF;
  private boolean allowEquals = true;

  WhereRange(WhereLogic parent, String formula) {
    super(parent);
    this.formula = formula;
  }

  public WhereRange<T> allowEquals(boolean allow) {
    this.allowEquals = allow;
    return this;
  }

  public WhereRange<T> min(T value) {
    min = value;
    return this;
  }

  public WhereRange<T> max(T value) {
    max = value;
    return this;
  }

  public WhereRange<T> minBy(String formula) {
    minF = formula;
    return this;
  }

  public WhereRange<T> maxBy(String formula) {
    maxF = formula;
    return this;
  }

  @Override
  public String toString() {
    return formula + (null == max ? null == min ? " BETWEEN ( ? )" :
      (allowEquals ? " >= " : " > ") + min : null == min ? (allowEquals ? " <= " : " < ") + max :
      " BETWEEN ( " + min + (allowEquals ? " < " : " <= ") + max + " )");
  }

  Filter createFilter(Temp view) {
    final Collection<Col> smallToLarge = new ArrayList<Col>(3);
    if (null != minF) smallToLarge.add(view.getCol(minF));
    else if (null != min) smallToLarge.add(Col.getConst(Comparable.class, min));
    smallToLarge.add(view.getCol(formula));
    if (null != maxF) smallToLarge.add(view.getCol(maxF));
    else if (null != max) smallToLarge.add(Col.getConst(Comparable.class, max));
    final Col[] cols = Col.toArray(smallToLarge);
    return allowEquals ? new FilterEq(cols) : new FilterNotEq(cols);
  }

  private static class FilterEq implements Filter {
    private final Col[] smallToLarge;

    private FilterEq(Col[] smallToLarge) {
      this.smallToLarge = smallToLarge;
    }

    @SuppressWarnings("unchecked")
    public Boolean is(int row) {
      Object value = smallToLarge[0].getRow(row);
      if (null == value) return false;
      final boolean compare = value instanceof Comparable;
      Comparable smaller = compare ? (Comparable) value : String.valueOf(value), larger;
      for (int i = 1, n = smallToLarge.length; i < n; i++) {
        value = smallToLarge[i].getRow(row);
        if (null == value || notMatch(smaller.compareTo(
          larger = compare ? (Comparable) value : String.valueOf(value)))) return false;
        smaller = larger;
      }
      return true;
    }

    boolean notMatch(int c) {
      return 0 < c;
    }
  }

  private static final class FilterNotEq extends FilterEq {
    private FilterNotEq(Col[] smallToLarge) {
      super(smallToLarge);
    }

    @Override
    boolean notMatch(int c) {
      return 0 <= c;//not allow equals
    }
  }
}

final class WhereLike extends Where implements QWhere.LikeText {
  private static final String[] TYPE = {"startWith", "startBy", "hasWith", "hasBy", "endWith", "endBy"};
  @SuppressWarnings("unchecked")
  private final Collection<String>[] value = new Collection[TYPE.length];
  private final String formula;
  private boolean ignoreCase;

  WhereLike(WhereLogic parent, String formula) {
    super(parent);
    this.formula = formula;
  }

  private WhereLike add(int index, String[] values) {
    Collection<String> coll = value[index];
    if (null == coll) coll = value[index] = new ArrayList<String>();
    coll.addAll(Arrays.asList(values));
    return this;
  }

  public WhereLike ignoreCase(boolean ignore) {
    this.ignoreCase = ignore;
    return this;
  }

  public WhereLike startWith(String... text) {
    return add(0, text);
  }

  public WhereLike startBy(String... formula) {
    return add(1, formula);
  }

  public WhereLike hasWith(String... text) {
    return add(2, text);
  }

  public WhereLike hasBy(String... formula) {
    return add(3, formula);
  }

  public WhereLike endWith(String... text) {
    return add(4, text);
  }

  public WhereLike endBy(String... formula) {
    return add(5, formula);
  }

  @Override
  @SuppressWarnings("unchecked")
  public String toString() {
    final Map<String, Collection<String>> temp = new LinkedHashMap<String, Collection<String>>();
    for (int i = 0, n = TYPE.length; i < n; i++) if (null != value[i]) temp.put(TYPE[i], value[i]);
    return formula + (ignoreCase ? " LIKE+ " : " LIKE ") + temp;
  }

  Filter createFilter(Temp view) {
    return new FilterLike(ignoreCase, view, formula, value);
  }

  private static final class FilterLike implements Filter {
    private final boolean toUpper;
    private final Col get;
    private final String[] starts, contains, ends;
    private final Col[] start, contain, end;

    private FilterLike(boolean toUpper, Temp view, String formula, Collection<String>[] value) {
      this.toUpper = toUpper;
      this.get = view.getCol(formula);
      starts = toArray(value[0], toUpper);
      start = view.getCols(toArray(value[1], false));
      contains = toArray(value[2], toUpper);
      contain = view.getCols(toArray(value[3], false));
      ends = toArray(value[4], toUpper);
      end = view.getCols(toArray(value[5], false));
    }

    private static String[] toArray(Collection<String> value, boolean toUpper) {
      if (null == value) return null;
      final String[] result = StringUtil.toArray(value);
      if (toUpper) for (int i = 0, n = result.length; i < n; i++) result[i] = result[i].toUpperCase();
      return result;
    }

    private String txt(Object value) {
      return toUpper ? String.valueOf(value).toUpperCase() : String.valueOf(value);
    }

    public Boolean is(int row) {
      final Object target = get.getRow(row);
      if (null == target) return false;
      final String text = txt(target);
      if (null != starts) for (final String s : starts) if (text.startsWith(s)) return true;
      if (null != ends) for (final String s : ends) if (text.endsWith(s)) return true;
      if (null != start) for (final Col c : start) {
        final Object got = c.getRow(row);
        if (null != got && text.startsWith(txt(got))) return true;
      }
      if (null != end) for (final Col c : end) {
        final Object got = c.getRow(row);
        if (null != got && text.endsWith(txt(got))) return true;
      }
      if (null != contains) for (final String s : contains) if (text.contains(s)) return true;
      if (null != contain) for (final Col c : contain) {
        final Object got = c.getRow(row);
        if (null != got && text.contains(txt(got))) return true;
      }
      return false;
    }
  }
}

final class WherePattern extends Where implements QWhere.LikePattern {
  private final String formula;
  private Collection<String> glob, regexp;

  WherePattern(WhereLogic parent, String formula) {
    super(parent);
    this.formula = formula;
  }

  private static String glob2Regex(char[] pattern) {
    final int n = pattern.length;
    final StringBuilder sb = new StringBuilder(n);
    int inGroup = 0, inClass = 0, firstIndexInClass = -1;
    for (int i = 0; i < n; i++) {
      final char ch = pattern[i];
      switch (ch) {
        case '\\':
          if (++i >= n) sb.append(ch);
          else {
            final char next = pattern[i];
            switch (next) {
              case ',':
                break;//won't escape
              case 'Q':
              case 'E':
                sb.append(ch);//extra escape
              default:
                sb.append(ch);
            }
            sb.append(next);
          }
          break;
        case '*':
          if (inClass == 0) sb.append('.');
          sb.append(ch);
          break;
        case '?':
          sb.append(inClass == 0 ? '.' : ch);
          break;
        case '!':
          sb.append(firstIndexInClass == i ? '^' : ch);
          break;
        case ',':
          sb.append(inGroup > 0 ? '|' : ch);
          break;
        case '.':
        case '(':
        case ')':
        case '+':
        case '|':
        case '^':
        case '$':
        case '@':
        case '%':
          if (inClass == 0 || (ch == '^' && firstIndexInClass == i)) sb.append('\\');
          sb.append(ch);
          break;
        case '[':
          inClass++;
          firstIndexInClass = i + 1;
          sb.append(ch);
          break;
        case ']':
          inClass--;
          sb.append(ch);
          break;
        case '{':
          inGroup++;
          sb.append(ch);
          break;
        case '}':
          inGroup--;
          sb.append(ch);
          break;
        default:
          sb.append(ch);
      }
    }
    return sb.toString();
  }

  private Collection<String> with(boolean isGlob) {
    return isGlob ? null == glob ? glob = new ArrayList<String>() : glob :
      null == regexp ? regexp = new ArrayList<String>() : regexp;
  }

  public LikePattern glob(String globPattern) {
    with(true).add(globPattern);
    return this;
  }

  public LikePattern glob(String... globPattern) {
    with(true).addAll(Arrays.asList(globPattern));
    return this;
  }

  public LikePattern regexp(String regexpPattern) {
    with(false).add(regexpPattern);
    return this;
  }

  public LikePattern regexp(String... regexpPattern) {
    with(false).addAll(Arrays.asList(regexpPattern));
    return this;
  }

  public String toString() {
    final Map<String, Collection<String>> temp = new LinkedHashMap<String, Collection<String>>();
    if (null != glob) temp.put("glob", glob);
    if (null != regexp) temp.put("regexp", regexp);
    return formula + " LIKE " + temp;
  }

  Filter createFilter(Temp view) {
    final Collection<Pattern> pattern = new ArrayList<Pattern>();
    if (null != glob) for (final String g : glob) pattern.add(Pattern.compile(glob2Regex(g.toCharArray())));
    if (null != regexp) for (final String r : regexp) pattern.add(Pattern.compile(r));
    return new FilterPattern(view.getCol(formula), pattern.toArray(new Pattern[pattern.size()]));
  }

  private static final class FilterPattern implements Filter {
    private final Col get;
    private final Pattern[] patterns;

    public FilterPattern(Col get, Pattern[] patterns) {
      this.get = get;
      this.patterns = patterns;
    }

    public Boolean is(int row) {
      final Object target = get.getRow(row);
      if (null == target) return false;
      final String text = String.valueOf(target);
      for (final Pattern pattern : patterns) if (pattern.matcher(text).matches()) return true;
      return false;
    }
  }
}

final class WhereCustom extends Where {
  private final Custom rule;

  WhereCustom(WhereLogic parent, Custom rule) {
    super(parent);
    this.rule = rule;
  }

  @Override
  public String toString() {
    return "MATCH_RULE(" + StringUtil.join(rule.getFormula(), ", ") + '@' + rule + ')';
  }

  Filter createFilter(Temp view) {
    return new FilterCustom(rule, view.getCols(rule.getFormula()));
  }

  private static final class FilterCustom implements Filter {
    private final Custom rule;
    private final Col[] col;
    private final int cols;

    private FilterCustom(Custom rule, Col[] col) {
      this.rule = rule;
      this.col = col;
      this.cols = col.length;
    }

    public Boolean is(int row) {
      return rule.is(Output.getValues(row, cols, col, new Object[cols]));
    }
  }
}